﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Actors;
using System.Threading.Actors.Helpers;
using System.Collections.Generic;

namespace TestActorMethods
{
    [TestClass]
    public class ListActorUnitTests
    {
        // Helper methods
        private String _listName = "TestList";

        private Object[] P(params Object[] parameters)
        {
            Object[] array = new Object[parameters.Length + 1];
            array[0] = _listName;
            Array.Copy(parameters, 0, array, 1, parameters.Length);
            return array;
        }

        private static void AssertListEqual(List<object> exp, List<object> actual)
        {
            Assert.AreEqual(exp.Count, actual.Count, string.Format("result length {0} does not match expected length {1}", actual.Count, exp.Count));
            for (int i = 0; i < exp.Count; i++)
            {
                Assert.AreEqual(exp[i], actual[i], string.Format("result at index {1} value {0} does not match expected value {2}", actual[i], i, exp[i]));
            }
        }

        [TestMethod]
        public void List_ClearTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_ClearTest_impl);
        }

        private void List_ClearTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Testing List clear ({0}).", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);

            ListHelperMethods.TryConstruct(list, P());

            Assert.AreEqual(ListHelperMethods.InsertAt(list, P(0, "0")), 1);
            Assert.AreEqual(ListHelperMethods.InsertAt(list, P(1, "1")), 2);
            Assert.AreEqual(ListHelperMethods.InsertAt(list, P(0, "-1")), 3);

            Assert.IsNull(ListHelperMethods.Clear(list, P()));
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);
            Assert.IsNull(ListHelperMethods.Clear(list, P()));  // Calling Clear a second time.

            // Must flush state/changes before they will be "visible"
            // to the next test.
            var waiter = list.Flush();
            if (waiter != null) waiter.Wait();

            // This is checking the actor state for extra, dangling items.  IE, if Clear leaks
            // nodes into our IActorState, we should know about it.  If this fails though, it could
            // be because we've added a new "field" to IActorState or some other reasonable issue.
            // Feel free to update the number below.
            if (list is StandaloneActorState)
            {
                const int NumKnownFieldsAndInstanceSpecificMethodsInListActor = 8;
                int numItemsInState = ((StandaloneActorState)list).NumItemsInState;
                if (numItemsInState != NumKnownFieldsAndInstanceSpecificMethodsInListActor)
                    throw new Exception(String.Format("Warning: Update this if you've added or removed \"fields\" from the actor.  After calling Clear, expected " +
                                                      "all state to be freed, except for the {0} fields & instance-specific methods used by ListActor.  Number of items left in state: {1}",
                                                      NumKnownFieldsAndInstanceSpecificMethodsInListActor, numItemsInState));
            }

            Assert.IsNull(ListHelperMethods.Destroy(list, P()));

            waiter = list.Flush();
            if (waiter != null) waiter.Wait();

            // Ensure that all our fields are erased.
            if (list is StandaloneActorState)
            {
                int numItemsInState = ((StandaloneActorState)list).NumItemsInState;
                if (numItemsInState != 0)
                    throw new Exception(String.Format("After Destroying the actor, there is at least one piece of state left in the IActorState.  Number of items leaked: {0}", 
                                                      numItemsInState));
            }
        }

        [TestMethod]
        public void List_StringTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_StringTest_impl);
        }

        private void List_StringTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Testing strings in common ways ({0}).", stateType);

            IActorState list = ActorUnitTests.CreateActorState(stateType);

            Assert.IsTrue((bool) ListHelperMethods.TryConstruct(list, new Object[] { _listName }));
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", "Third")), 1);
            Assert.AreEqual(ListHelperMethods.Push(list, P("right", "Second")), 2);
            Assert.AreEqual(ListHelperMethods.Push(list, P("right", "First")), 3);

            Object ret;
            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            List<Object> snapshot = (List<Object>)ret;
            List<Object> expected = new List<Object> { "Third", "Second", "First" };
            AssertListEqual(expected, snapshot);

            Assert.AreEqual(ListHelperMethods.Length(list, P()), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("2")), "First");
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("1")), "Second");
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("0")), "Third");

            Assert.IsNull(ListHelperMethods.Sort(list, P()));

            Console.WriteLine("Testing contents after sorting");
            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            snapshot = (List<Object>)ret;
            expected = new List<Object> { "First", "Second", "Third" };
            AssertListEqual(expected, snapshot);

            Assert.AreEqual(ListHelperMethods.Length(list, P()), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("0")), "First");
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), "Second");
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("2")), "Third");
            Assert.AreEqual(ListHelperMethods.IndexOf(list, P("First")), 0);
            Assert.AreEqual(ListHelperMethods.IndexOf(list, P("Second")), 1);
            Assert.AreEqual(ListHelperMethods.IndexOf(list, P("Third")), 2);

            // Look at Remove now.
            Console.WriteLine();
            Console.WriteLine("Testing Remove & RemoveAt");
            Assert.AreEqual(ListHelperMethods.Push(list, P("right", "First")), 4);
            Assert.AreEqual(ListHelperMethods.InsertAt(list, P(2, "Fourth")), 5);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(2)), "Fourth");  // Make sure things happened corectly.
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(4)), "First");
            Assert.AreEqual(ListHelperMethods.RemoveAt(list, P(2)), "Fourth");
            Assert.AreEqual(ListHelperMethods.Remove(list, P(0, "First")), 2);  // Remove all occurrences of First
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 2);
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", "Third")), 3);
            Assert.AreEqual(ListHelperMethods.Remove(list, P("1", "Third")), 1);  // Remove first occurrence of Third.

            Console.WriteLine("Enumerating contents after adding & removing several items");
            snapshot = (List<Object>)ListHelperMethods.Enumerate(list, P(0, -1));
            expected = new List<Object> { "Second", "Third" };
            AssertListEqual(expected, snapshot);

            Assert.AreEqual(ListHelperMethods.GetAt(list, P("0")), "Second");
            Assert.AreEqual(ListHelperMethods.GetAt(list, P("1")), "Third");

            // Clean up list using RemoveAt
            Assert.AreEqual(ListHelperMethods.RemoveAt(list, P("0")), "Second");
            Assert.AreEqual(ListHelperMethods.RemoveAt(list, P(0)), "Third");

            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);
            snapshot = (List<Object>)ListHelperMethods.Enumerate(list, P(0, -1));
            if (snapshot.Count != 0)
                throw new Exception(String.Format("After removing all items, the enumerate method should have returned an empty list, but it had {0} items.", snapshot.Count));
        }

        [TestMethod]
        public void List_NullTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_NullTest_impl);
        }

        private void List_NullTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Null test ({0})", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);
            ListHelperMethods.TryConstruct(list, P());

            Console.WriteLine("Checking a few boundary cases around null.");
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", null)), 1);
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", null)), 2);
            Assert.AreEqual(ListHelperMethods.IndexOf(list, P(new Object[] { null })), 0);
            Assert.IsNull(ListHelperMethods.GetAt(list, P(0)), null);
            Assert.IsNull(ListHelperMethods.GetAt(list, P("1")), null);           
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 2);
            
            Assert.AreEqual(ListHelperMethods.Remove(list, P(2, null)), 2);
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);            
        }

        // This was an excerpt from a stress test that was failing.  Calling InsertAt
        // with an index = list length was neglecting to patch up the (previously) final element's
        // "next" pointer.
        [TestMethod]
        public void List_InsertUpdateTest()
        {
            Console.WriteLine("InsertUpdate test");
            int filler = 10, contentionIndex = 10;
            IActorState list = ActorUnitTests.CreateActorState();
            ListHelperMethods.TryConstruct(list, P());

            for (int i = 0; i < filler; i++)
            {
                Assert.AreEqual(ListHelperMethods.Push(list, P("right", "xxx" + i)), i + 1);
            }

            for (int i = 0; i < 10; i++)
            {
                Assert.AreEqual(ListHelperMethods.InsertAt(list, P(contentionIndex, "yyy" + i)), filler + i + 1);
                Assert.IsNull(ListHelperMethods.UpdateItemAt(list, P(contentionIndex, "zzz" + i)));
            }

            Object ret;
            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            List<Object> snapshot = (List<Object>)ret;
            List<Object> expected = new List<Object> { "xxx0", "xxx1", "xxx2", "xxx3", "xxx4", "xxx5", "xxx6", "xxx7", "xxx8", "xxx9",
                                                       "zzz9", "zzz8", "zzz7", "zzz6", "zzz5", "zzz4", "zzz3", "zzz2", "zzz1", "zzz0", };
            AssertListEqual(expected, snapshot);
        }

        [TestMethod]
        public void List_RemoveRangeTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_RemoveRangeTest_impl);
        }

        private void List_RemoveRangeTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("RemoveRange test ({0})", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);
            ListHelperMethods.TryConstruct(list, P());

            int num = 6;
            for (int i = 0; i < num; i++)
                Assert.AreEqual(ListHelperMethods.Push(list, P("right", i)), i + 1);
            ListHelperMethods.RemoveRange(list, P(2, 3));

            Assert.AreEqual(ListHelperMethods.Length(list, P()), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 0);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 1);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(2)), 5);

            ActorUnitTests.ExpectException<ArgumentOutOfRangeException>(ListHelperMethods.RemoveRange, list, P(0, -1));
            ActorUnitTests.ExpectException<ArgumentOutOfRangeException>(ListHelperMethods.RemoveRange, list, P(-1, 1));
            ActorUnitTests.ExpectException<ArgumentOutOfRangeException>(ListHelperMethods.RemoveRange, list, P(0, Int32.MaxValue));
            ActorUnitTests.ExpectException<ArgumentException>(ListHelperMethods.RemoveRange, list, P(2, 2));

            ListHelperMethods.RemoveRange(list, P(0, 3));
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);
        }

        [TestMethod]
        public void List_TrimTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_TrimTest_impl);
        }
        
        private void List_TrimTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Trim test({0})", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);

            int num = 7;
            for (int i = 0; i < num; i++)
                Assert.AreEqual(ListHelperMethods.Push(list, P("right", i)), i + 1);
            ListHelperMethods.Trim(list, P(2, 5));  // Extract the sub-list containing 2, 3, 4, 5

            Assert.AreEqual(ListHelperMethods.Length(list, P()), 4);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 2);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(2)), 4);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(3)), 5);

            ListHelperMethods.Trim(list, P(0, -1));  // Get existing list: 2, 3, 4, 5
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 4);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 2);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(2)), 4);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(3)), 5);

            ListHelperMethods.Trim(list, P(0, -2));  // Drop the last element: 2, 3, 4
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 2);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(2)), 4);

            ListHelperMethods.Trim(list, P(1, -1));  // Drop the first element: 3, 4
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 2);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 4);

            ListHelperMethods.Trim(list, P(-2, -1));  // Get the existing list, using negative indexes: 3, 4
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 2);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(0)), 3);
            Assert.AreEqual(ListHelperMethods.GetAt(list, P(1)), 4);

            ListHelperMethods.Trim(list, P(1, 0));  // Return the empty list because start > end
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);

            ListHelperMethods.Push(list, P("left", 7));

            ListHelperMethods.Trim(list, P(100, 101));  // Return the empty list becauuse start > list.Count
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);
        }


        [TestMethod]
        public void List_PopTest()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_PopTest_impl);
        }

        private void List_PopTest_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Testing Pop and Push multiple ({0}).", stateType);

            IActorState list = ActorUnitTests.CreateActorState(stateType);

            // push multiple right
            Assert.IsTrue((bool)ListHelperMethods.TryConstruct(list, new Object[] { _listName }));
            Assert.AreEqual(ListHelperMethods.Push(list, P("right", "First", "Second", "Third", "Fourth", "Fifth")), 5);

            Object ret;
            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            List<Object> snapshot = (List<Object>)ret;
            List<Object> expected = new List<Object> { "First", "Second", "Third", "Fourth", "Fifth" };
            AssertListEqual(expected, snapshot);

            // test pop right and left
            Assert.AreEqual(ListHelperMethods.Pop(list, P("right")), "Fifth");
            Assert.AreEqual(ListHelperMethods.Pop(list, P("right")), "Fourth");
            Assert.AreEqual(ListHelperMethods.Pop(list, P("left")), "First");
            Assert.AreEqual(ListHelperMethods.Pop(list, P("left")), "Second");

            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            snapshot = (List<Object>)ret;
            expected = new List<Object> { "Third" };
            AssertListEqual(expected, snapshot);

            Assert.AreEqual(ListHelperMethods.Pop(list, P("right")), "Third");
            Assert.AreEqual(ListHelperMethods.Length(list, P()), 0);

            // push multiple left
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", "Fifth", "Fourth", "Third", "Second", "First")), 5);

            ret = ListHelperMethods.Enumerate(list, P(0, -1));
            snapshot = (List<Object>)ret;
            expected = new List<Object> { "First", "Second", "Third", "Fourth", "Fifth" };
            AssertListEqual(expected, snapshot);
        }

        // Test added in the process of catching a bug on the initial InsertAt
        [TestMethod]
        public void List_InitialInsertAtTest()
        {
            Console.WriteLine("Initial InsertAt Test");
            IActorState list = ActorUnitTests.CreateActorState();
            Assert.AreEqual(ListHelperMethods.InsertAt(list, P(0, "foo")), 1);
        }

        [TestMethod]
        public void List_PushLeftMultiple()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_PushLeftMultiple_impl);
        }
        
        private void List_PushLeftMultiple_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Testing pushing multiple items on the left side of the list ({0})", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);

            Assert.IsTrue((bool)ListHelperMethods.TryConstruct(list, new Object[] { _listName }));

            // push multiple left in separate calls.
            Assert.AreEqual(1, ListHelperMethods.Push(list, P("left", 3)));
            Assert.AreEqual(2, ListHelperMethods.Push(list, P("left", 2)));
            Assert.AreEqual(3, ListHelperMethods.Push(list, P("left", 1)));
            Assert.AreEqual(3, ListHelperMethods.Length(list, P()));
            Assert.AreEqual(1, (int) ListHelperMethods.GetAt(list, P(0)));
            Assert.AreEqual(2, (int)ListHelperMethods.GetAt(list, P(1)));
            Assert.AreEqual(3, (int)ListHelperMethods.GetAt(list, P(2)));

            ListHelperMethods.Clear(list, P());
            // push multiple left in one call.
            Assert.AreEqual(ListHelperMethods.Push(list, P("left", "Fifth", "Fourth", "Third", "Second", "First")), 5);
            Assert.AreEqual(5, ListHelperMethods.Length(list, P()));
            Assert.AreEqual("First", (String)ListHelperMethods.GetAt(list, P(0)));
            Assert.AreEqual("Second", (String)ListHelperMethods.GetAt(list, P(1)));
            Assert.AreEqual("Third", (String)ListHelperMethods.GetAt(list, P(2)));
            Assert.AreEqual("Fourth", (String)ListHelperMethods.GetAt(list, P(3)));
            Assert.AreEqual("Fifth", (String)ListHelperMethods.GetAt(list, P(4)));
        }

        [TestMethod]
        public void List_AddOnce()
        {
            ActorUnitTests.TestAcrossAllStateTypes(List_AddOnce_impl);
        }

        private void List_AddOnce_impl(ActorStateEnum stateType)
        {
            Console.WriteLine("Testing an AddOnce method that adds a set of items to a list, if and only if they don't already exist. ({0})", stateType);
            IActorState list = ActorUnitTests.CreateActorState(stateType);

            Assert.IsTrue((bool)ListHelperMethods.TryConstruct(list, new Object[] { _listName }));

            // Let's add odd numbers to an empty list.
            Assert.AreEqual(5, ListHelperMethods.AddOnce(list, P(1, 3, 5, 7, 9)));
            Assert.AreEqual(1, ListHelperMethods.GetAt(list, P(0)));
            Assert.AreEqual(5, ListHelperMethods.Length(list, P()));

            // Now add all numbers 0 - 10.
            Assert.AreEqual(6, ListHelperMethods.AddOnce(list, P(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)));
            Assert.AreEqual(11, ListHelperMethods.Length(list, P()));

            ListHelperMethods.Sort(list, P());

            for (int i = 0; i < 11; i++)
                Assert.AreEqual(i, (int)ListHelperMethods.GetAt(list, P(i)));

            // Note: We are NOT testing what happens when the list we're adding contains duplicates itself!
            // Can we assume people won't do that?
        }
    }
}
